Skip to content

feat: budgeted console formatter (inspect builtin) - #416

Merged
NathanWalker merged 2 commits into
mainfrom
feat/inspect
Jul 31, 2026
Merged

feat: budgeted console formatter (inspect builtin)#416
NathanWalker merged 2 commits into
mainfrom
feat/inspect

Conversation

@edusperoni

@edusperoni edusperoni commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

What

Replaces the console's no-DevTools formatting pipeline — which could hang the app on a single console.log — with internal/inspect.js, a budgeted util.inspect-lite built on primordials. Stacked on #415 (primordials), which is stacked on #411 (builtins).

The problem

console.log(obj) without a DevTools frontend serialized the whole object graph:

  • JSON.stringify(obj, replacer, 2) with no depth or size limit — logging a store/component tree serialized everything reachable, then shipped megabytes through a stack-frame regex and NSLog.
  • The replacer's seen array made cycle tracking O(n²) — quadratic interpreter work under jitless — and never popped on subtree exit, so shared acyclic references falsely printed [Circular].
  • A separate hand-rolled array printer recursed with no depth limit and only caught an array containing itself — any indirect cycle was a stack overflow.
  • JSON.stringify invokes getters and toJSON — a log line could execute arbitrary user code.
  • console.dir had a third bespoke dump with the same issues.

The fix

inspect.js (function-body builtin, primordials-clean — it must work precisely when the app is broken):

  • Budgets everywhere: depth 2 (dir uses 4), 100 entries per collection, 10k chars per string, 16KB hard cap per call with an explicit truncation marker. A pathological log is now bounded work by construction.
  • Correct cycles: ancestor Set (enter/exit) — true cycles say [Circular], diamonds print normally, O(1) membership.
  • Getters are never invoked ([Getter]/[Setter] tags). Two deliberate exceptions: a guarded error.stack read (V8 materializes stacks via a lazy accessor and every error path in the runtime already reads it), and custom toString overrides — NativeScript core's ViewBase/Observable convention (Button(42)-style short forms) is honored, matching the previous console: an own-or-inherited toString other than Object.prototype's is detected via descriptors (no invocation to detect), then invoked guarded and capped; broken overrides degrade to structural rendering.
  • Tamper-immune: brands via captured Object.prototype.toString, Map/Set walked through captured iterator next (early-exit capable, unlike forEach), sizes via captured accessor getters. Verified by device tests that break Array.prototype.*, Object.keys, JSON.stringify etc. and log anyway.
  • Native wrappers render as short hints ([UILabel], [class UIView], [Pointer]) via a binding.getNativeWrapperHint native callback instead of walking native-backed graphs.
  • Type-aware: Map/Set with sizes, Date/RegExp/Error (with stack), [Function: name]/[class Name], TypedArrays/ArrayBuffers with lengths, null-prototype and constructor-name prefixes, sparse arrays, BigInt, symbols.
  • Exposed as the internal __inspect global (testability + app-level escape hatch).

C++ side shrinks: console.log/dir/assert all route through one cached formatter; the array printer, the dir dump, and smart-stringify.js (+ its Helpers/Caches plumbing) are deleted. Top-level strings still print raw. If the formatter ever fails to initialize, logging degrades to ToDetailString instead of breaking.

Also extends primordials.js (Set methods, iterator next captures, accessor getters, brand/regexp helpers) and the ESLint captured-statics list.

Output changes to be aware of

Logs now look like Node's inspect output — { a: 1, b: [Circular] }, Map(3) { ... }, ... 150 more items, truncation markers — instead of pretty-printed JSON. console.dir keeps its dump markers but uses depth 4.

Testing

  • 31 host-side functional assertions (Node harness over the same function bodies), including hostile stack getters and tampered prototypes.
  • 9 new device specs (InspectTests.js): cycles vs diamonds, caps, getter non-invocation, native-wrapper hints, tampered-prototype formatting, and a regression test that console.log of a 5000-node cyclic graph completes in bounded time.
  • Full suite: 922 tests, 0 failures (912 baseline + 10 new).

Also fixes a latent js2c bug this PR flushed out: builtin sources containing non-ASCII bytes (UTF-8 >= 0x80) failed to compile as const char array initializers; the generator now emits unsigned char.

Summary by CodeRabbit

  • New Features

    • Improved console output with a safer, richer object inspector.
    • Added formatting for collections, dates, errors, functions, native objects, circular references, and typed data.
    • Added output limits and getter-safe formatting to prevent excessive or unexpected evaluation.
  • Bug Fixes

    • Improved handling of custom formatting failures and complex object structures.
    • Console output is more resilient to modified built-in prototypes.
  • Documentation

    • Documented the new inspection formatter and its safety protections.
  • Tests

    • Added comprehensive coverage for inspection, truncation, cycles, getters, and native objects.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d1a1a216-1dbd-41d9-b6a1-7eb5a8029ba6

📥 Commits

Reviewing files that changed from the base of the PR and between d5c187c and 9720d56.

📒 Files selected for processing (15)
  • NativeScript/runtime/BuiltinLoader.cpp
  • NativeScript/runtime/Caches.h
  • NativeScript/runtime/Console.cpp
  • NativeScript/runtime/Console.h
  • NativeScript/runtime/Helpers.h
  • NativeScript/runtime/Helpers.mm
  • NativeScript/runtime/js/README.md
  • NativeScript/runtime/js/inspect.js
  • NativeScript/runtime/js/primordials.js
  • NativeScript/runtime/js/smart-stringify.js
  • TestRunner/app/tests/InspectTests.js
  • TestRunner/app/tests/index.js
  • eslint.config.mjs
  • tools/js2c-inputs.xcfilelist
  • tools/js2c.mjs

📝 Walkthrough

Walkthrough

The runtime replaces smart-stringify with a hardened inspect.js formatter. Console methods use the formatter for objects and functions. Primordials, native-wrapper hints, builtin packaging, documentation, and comprehensive tests were updated.

Changes

Inspect formatter integration

Layer / File(s) Summary
Inspect formatter and primordial snapshot
NativeScript/runtime/js/inspect.js, NativeScript/runtime/js/primordials.js, NativeScript/runtime/js/README.md, eslint.config.mjs
Adds bounded formatting for JavaScript values, native wrappers, cycles, collections, accessors, errors, and tampered built-ins. Captures the required primordial operations.
Console inspect integration
NativeScript/runtime/Console.*, NativeScript/runtime/Helpers.*, NativeScript/runtime/Caches.h
Initializes and caches __inspect, routes console object formatting through it, adds native-wrapper hints, and removes the JSON stringification helpers.
Builtin packaging and runtime snapshot documentation
tools/js2c-inputs.xcfilelist, tools/js2c.mjs, NativeScript/runtime/BuiltinLoader.cpp
Packages inspect.js, removes smart-stringify.js, supports unsigned generated source bytes, and updates the primordial snapshot comment.
Inspect behavior validation
TestRunner/app/tests/InspectTests.js, TestRunner/app/tests/index.js
Adds inspect coverage for formatting, limits, getter safety, native objects, cycles, custom toString, performance, and tampered built-ins.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Console
  participant BuiltinLoader
  participant Inspect
  participant NativeWrapper
  Console->>BuiltinLoader: initialize inspect builtin
  BuiltinLoader->>Inspect: execute inspect.js
  Inspect-->>Console: cache formatter function
  Console->>Inspect: format object or function
  Inspect->>NativeWrapper: request wrapper hint when applicable
  NativeWrapper-->>Inspect: return native hint
  Inspect-->>Console: return bounded output
Loading

Possibly related PRs

  • NativeScript/ios#411: Shares the builtin-loader and js2c infrastructure replaced by this inspect integration.
  • NativeScript/ios#415: Shares the primordial snapshot infrastructure extended for the inspect formatter.

Suggested reviewers: nathanwalker

Poem

A rabbit inspects each leafy new line,
With primordials captured and output kept fine.
Cycles and getters stay safely in sight,
Native hints hop into the light.
Console now formats with a careful delight.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Base automatically changed from feat/primordials to main July 31, 2026 05:35
…ed logging

console.log without DevTools serialized entire object graphs through
JSON.stringify with an O(n^2) seen-array, no depth or size limits, a
hand-rolled array printer that only caught direct self-cycles, and a
third bespoke dump in console.dir. A large object could hang the app
for seconds and shared acyclic references printed as [Circular].

internal/inspect.js is a util.inspect-lite built on primordials: depth,
per-collection and total-output budgets make unbounded work impossible;
an ancestor set reports only true cycles; getters are never invoked
(except the guarded error.stack read every error path already does);
brands come from Object.prototype.toString so tampered prototypes
cannot break or hijack logging. Native wrappers render as a short
class-name hint via the binding bag instead of being walked. Console
log/dir/assert all route through it, smart-stringify is retired, and
the formatter doubles as the internal __inspect global.
@NathanWalker
NathanWalker merged commit 5e40702 into main Jul 31, 2026
7 of 8 checks passed
@NathanWalker
NathanWalker deleted the feat/inspect branch July 31, 2026 05:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants